Model GLM-5.2 (glm_moe_dsa): prefill, MTP chain, per-op precision - #104
Conversation
Fork the sparse-MoE predicted execution graph into a new glm_moe_dsa family specialised for zai-org/GLM-5.2 (GlmMoeDsaForCausalLM). Built from config.json and the checkpoint's safetensors index only — no traces. What the fork prices that the V4 graph could not: - MLA attention: one kv_lora_rank=512 latent shared across 64 query heads (KV traffic derived from the latent, not num_key_value_heads); no CSA/HCA/SWA compression schedule — every layer runs the same DSA attention. - IndexShare: only 21 of 78 layers compute the lightning indexer; the 57 'shared' layers reuse a neighbour's top-2048 selection and carry no indexer weights (proven from the weight map). Emitting indexer nodes on full layers only keeps the indexer at ~0.4% instead of a 4x overcount. - dense-then-sparse MLP schedule (first_k_dense_replace=3), bf16 throughout (no fp4 expert default leaking in from V4), plain dense o_proj. model_weight_bytes validates to +0.1% against the published 1.507 TB checkpoint. Wiring: new GlmMoeDsaModelSpec + predict_glm_graph + spec/detector in glm_graph.py; catalogue entry models/glm-5.2.yaml; all eight dispatch sites in model_catalogue.py and registry.py (glm_moe_dsa detected before sparse_moe, since both carry index_topk + n_routed_experts — model_type is the separator). Deliverables under docs/glm-5.2/: standup BRIEF.md (end-to-end, headroom, RunPod) and three JSON graph artifacts. Tests in tests/test_glm_graph.py pin IndexShare, MLA KV sharing, the dense/sparse split, bf16 precision, footprint, and detection order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework the glm_moe_dsa runtime to the structure of the MiMo-V2.5 design note, for GLM-5.2's own architecture rather than by transcription. Graph: - op_dtype_overrides, read from GLM-5.2-FP8's modules_to_not_convert and moe_router_dtype. Three precisions in one block: fp8 backbone + experts, bf16 lm_head / eh_proj / *indexer*, fp32 router. The layout inverts the usual fp8 pattern (o_proj is quantised, the indexer is not), and the indexer is the node that owns 54% of a 1M-context step. - Prefill, with DSA asymptotics. index_topk bounds the core's FLOPs in both phases and its bytes in neither: at prefill each query selects a different top-k and their union is the whole cache. Four helpers rather than reusing BatchConfig.attention_qk_pairs, which is the dense causal count. - serial_launches on every node, so the launch bound exists. At B=1 it is 63% of the predicted floor. - Two collectives per layer, not one folded node -- at 688 kB the count is the cost. EP all-to-all now gated on the layer having experts; the three dense layers were being charged one. - MTP as a D-stage chain with an lm_head per stage, driven by --spec-tokens. GLM's draft block carries a full 256-expert bank, so the chain is 5.2% of the step, not the 1-2% a dense draft head costs. - Indexer wk + weights_proj in both the graph and the footprint. Planner: - _FP32_PEAKS: the router is the first fp32 op the planner has seen, and the A100 default was 3.4x low on an H200. - gitm plan keeps the node's own bound (854 launch-bound nodes printed as memory-bound) and prints one ridge per dtype present. Catalogue: glm-5.2-fp8 (the vendor's recommended shape, 753.33 GB, +0.34% against the published checkpoint); glm-5.2 keeps the bf16 model fact (1.507 TB, +0.08%) and gains the fp32 router. Docs: BRIEF.md -> DESIGN-NOTE.md, restructured to the note's eight sections plus appendix. Deletes 29k lines of committed node-dump JSON that goes stale on every graph change; the commands at the top regenerate any of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two corrections found by reading Appendix A's per-node dump against the graph, both in the node the note's headline claim is about. - attn_index_score dropped index_n_heads in the prefill rewrite, scoring each candidate once instead of once per index head. The 32 heads each dot against the single shared 128-d key wk produces per token (MQA-style), which is why the key bytes carry no head factor and the score FLOPs do. The decode picture is unchanged -- the node was memory-bound at every context and stays so, AI 2.0 -> 64.0, and the scan is still 54.4% of a 1M step. Prefill moves: 5.77 TF against 22 MB of keys, emphatically compute-bound, and the whole-pass AI goes 341 -> 358. - The draft head ran on a pure-prefill step, adding 18 zero-work nodes whose only cost was their launches -- a launch facet made of kernels that never ran. A draft proposes continuations; a prefill chunk has nothing yet to continue. Also: the doc's prefill reproduce command now names the shape its table is labelled with (--batch 0 --kv-len 0), and every affected figure in the note is re-derived from the fixed graph. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The graph emitted 16 nodes per layer where the design note's spec lowers a layer to 23. The missing seven were all pointwise, and the note's entire low-batch argument is a claim about how many kernels a layer *is* -- so folding them into the GEMMs they precede reported a decode step as more memory-bound than it is. Now emitted, in issue order: input_layernorm, act_quant_attn, attn_residual, post_attention_layernorm, moe_sigmoid_bias (split from moe_topk, because they are the same size and the same cost and only one of them is a CUDA-graph hazard), act_quant_moe, moe_silu, mlp_silu, mlp_residual. Plus the prologue/epilogue the step actually has: embed_tokens, final_norm, and the logits_all_gather that a vocabulary-sharded lm_head forces onto the critical path before anything can be sampled. And mtp_norms for the draft block's enorm/hnorm. act_quant is emitted only where the consuming GEMM is genuinely fp8 -- GLM-5.2-FP8 declares activation_scheme "dynamic", and on the bf16 checkpoint the kernel does not exist. That is a difference a single model-wide dtype cannot express, and it falls out of op_dtype_overrides. What it changes: a MoE layer is 24 kernels and two of them cost anything. At B=32 the step goes 15.887 -> 17.173 ms with the launch facet at 17% (was 11%); at B=1 launches are 73% of the floor, not 63%, and 87% at the eager 5 us. Node count 1,294 -> 1,927. Every figure in the note is re-derived, and the layer's node sequence is now pinned by a test so the code cannot drift from Appendix A. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-up to the pointwise lowering. That commit added twelve op names without checking them against deviation.classify_op, which is the fallback identity for a capture with no NVTX ranges (docs/kernel_identity.md). A name it cannot classify leaves the predicted node permanently unmatched AND the real kernel filed as unmodeled -- two errors in opposite directions, in the per-op diff this family exists to support. The note asserts that diff in three places, so the names are load-bearing. Three of the new names could not resolve and three collided with decisions the other families depend on. Resolved by following the canonical names rather than redefining them: - One rms_norm op for all three norm sites. They are one kernel name; only an NVTX range can say which site a launch belongs to. The residual add is inside it, because vLLM runs RMSNorm.forward(x, residual) as one fused_add_rms_norm kernel -- a separate residual node predicted a launch that never happens. - SwiGLU folded back into mlp_gate_up and moe_routed: silu_and_mul was already that op's needle. - The fused gating kernel emitted as a second moe_router instance, not a private moe_topk: moe_align/topk_softmax -> moe_router is tested and the dense-MoE and hybrid families rely on it. It is still its own node -- it is still the only data-dependent shape in the step. _OP_RULES then gains only what is genuinely new and unclaimed: rms_norm, act_quant, embed_tokens, moe_permute/moe_combine, attn_index_proj (was shadowed by the scan's own "indexer" needle), attn_kv_b (absent while only absorbed MLA was modelled; GLM models it unabsorbed, so the kernel exists), mtp_eh_proj. A test asserts every op the graph emits resolves. Separately: attn_index_score carried one dtype for two questions. Its bytes follow how the keys are stored (fp8); its FLOPs follow what the indexer computes in, and the indexer is one of the modules the quantiser skipped. Invisible at decode -- memory-bound at every context -- and 1.2% -> 2.2% of prefill, where it is compute-bound. Full suite back to the pre-existing baseline of 4 failures. Every figure in the note re-derived; §7 gains the two gaps this pass actually was. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rigorous sweep for stale artifacts from the earlier attempts, plus a validation pass against the published config and the transformers docs. The real find: glm-5.2.yaml's indexer_types had 78 layers' worth of evidence in 77 entries. Layer 77 fell through to the modulo fallback and landed on the right answer, so nothing failed and the count still came out 21 -- a plausible total resting on evidence that is not there, which is the exact failure explicit schedules exist to prevent. Restored from the published array; the catalogue loader now validates schedule length the way spec_from_hf_config already did, and a test pins it. Related: the FP8 entry carried its own 78-entry copy of both schedules -- 158 lines of duplicated evidence, in the file where the drift happened. It now `extends: glm-5.2` (new, in model_catalogue), so the two entries differ only where the checkpoints do: four dtypes. provenance is deliberately not inherited; each checkpoint is validated against its own published size. Validated against HuggingFace, and two corrections fall out: - transformers documents indexer_types "shared" as reusing *the previous full layer's* top-k -- what the graph does, now quoted rather than asserted. - Z.ai publishes 744B params; the checkpoint is 753.3B by its own bytes. The gap is the MTP block, and this graph predicts 744.2B with the draft removed (0.03%). That puts the MTP block at 9.9B against 0.23B for a dense draft head -- confirming from a number published for another reason what the weight map already said: the draft is a full MoE block. - IndexShare's published figure is 2.9x whole-model per-token FLOPs at 1M; the 3.7x used here is the indexer's own ratio. Both now stated. Pruning: module docstring 80 -> 48 lines (it restated the note), the long comment blocks trimmed to the hazard they guard, glm_graph 1261 -> 1169. Doc: §2.3, §7.3 and §9 cut where they repeated the header or the hardware section; four stale op names fixed. Comment density unchanged at 17%, matching the sibling families. No behaviour change: the decode floor is 16.551 ms before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Follow-ups from reviewing the prune and the validation pass. The §9 prune took the `vllm serve` block with it, and the replacement pointed at "the recipe quoted in the hardware section" where it no longer was. That block is evidence, not prose: it is what makes the hardware assumption auditable as the vendor's own rather than mine, and §6.2's C6 and §6.4 row 0 are built on it. Restored into the hardware section, with the note that --enable-expert-parallel is NOT in the vendor recipe even though §4 prices the EP8 shape -- which is capture C5 and re-ranks the largest line in prefill. Two claims were stronger than the code supports: - `expected_stream_id=1` on collectives does not give "the stream-concurrency invariant something to read". docs/invariants.md §3 defines that invariant, but optimizer/monitor.py tests overlap using the *observed* kernel's stream_id; nothing reads the predicted field. It is a hook, not a wiring, and both the code comment and §7.1 now say so. - Reported as G11, found not fixed: kernels/library.yaml scopes every lever with `applies_to_kernels` from a canonical op list that has no `moe_routed`/`moe_shared`, so the two levers meant for routed-expert traffic scope to `[mlp_gate_up, mlp_down]` -- true of a dense FFN, false of either MoE family. §5 rank 5 targets 74% of a decode step through tooling that cannot match it. Pre-existing and shared with DeepSeek-V4, so it should land where both families can be checked. Also: §1 now carries the truncated-schedule incident as the worked demonstration of why the schedules are read verbatim -- the wrong evidence produced the right total and a byte-identical floor, which is the whole argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last "prune" was token-level and the doc grew back. This one deletes
rather than rewrites.
Doc 1208 -> 1047. Structural cuts, not word-smithing:
- The node list appeared three times -- §4.1's ASCII table, Appendix A.1's
markdown table, and §2.1's flowchart subgraph. It now appears once, in
A.1, with shapes and dtypes; §4.1 keeps the same nodes sorted by cost,
which is the argument, and §2.1's diagram points at A.1 instead of
redrawing it.
- §4.3 ("same kernel, opposite label") was §2.2's four-node table and
§3.2's bullets a third time. Its two unique rows moved into §2.2 and the
section is gone.
- §4.4 was a reverse index of §4.2's flip column; it is now the eight
variables that move more than one row, with magnitudes.
- §5.1's gate-check table, §6.1's four-box flowchart, §6.3's instrument
table and §7.0's "already gets right" table were all bookkeeping in table
form. Prose, one to four lines each.
- §1's ASCII layer diagram, and three paragraphs restating the table
immediately above them, deleted outright.
Composition is now 292 table + 110 diagram + 399 prose + headings/blanks:
the tables and diagrams are the content, and the prose no longer restates
them.
Code 1171 -> 1137, and two of those lines were dead. effective_kv_tokens
and index_candidates were the decode-only helpers superseded by
core_read_entries / index_scan_pairs when prefill landed; each was
referenced only by its own definition. The earlier artifact sweep missed
them because it grepped GLM names, not dead functions. Also dropped a local
_canon that duplicated roofline._canon_dtype.
No behaviour change: floor still 16.551 ms, suite still at the pre-existing
4 failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🤖 Claude Code ReviewReview of
|
Code Review by GeminiNo bugs or issues found. The code is well-structured, thoroughly documented, and the test suite is comprehensive. The changes correctly implement the GLM-5.2 model's unique architectural features, including per-op precision, DSA asymptotics for prefill, and the MTP chain. The fixes for existing planner gaps (G1-G9 from the design note) are also well-integrated. |
ruff UP038 on the isinstance tuple — the rest of the file already used X | Y, this one was missed. The review's substantive catch: break-even alpha was computed as (1.70-1)/D = 0.140, which follows BatchConfig.tokens_per_step's 1 + D*alpha. That is right for independent draws and wrong for speculative decoding, where the verifier accepts a prefix — expected accepted length is sum(alpha^i), not 1 + D*alpha. At D=5 the linear form overstates accepted tokens by 1.8x at alpha=0.5, and real break-even is 0.415, not 0.140. §3.3 now gives both rows and says which to read; propagated to §4.2, §4.3 and the open-questions table. Not changing BatchConfig — that convention is shared with every family. Also from the review: renumbered §4.4 to §4.3 (§4.3 was deleted as a duplicate and the gap was left), stated the node arithmetic explicitly (1,614 = 1,591 + 23, 1,706 = 1,591 + 115, and 192 rows = B x (1+D)), moved G11 into the gap table instead of leaving it as prose after it, promoted the expert-parallel question to Q1 and the prefill-tile question to Q3, and noted the reproduce commands are branch-relative. Declined the suggestion to add an ep_imbalance uncertainty range — it would mean inventing measurements we do not have, and A8 already says it is declared rather than fitted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 99512c7. Taking the review points in turn. Fixed
Not doing
lint was 🤖 Addressed by Claude Code |
🤖 Claude Code ReviewReviewThis diff is a 1,054-line documentation file — no executable code is changed. Feedback is scoped accordingly. 🐛 Bugs
🔒 Security
⚡ Performance
📊 Reproducibility
💡 Suggestions
|
Code Review by Gemini## Code Review
The changes introduce comprehensive support for the GLM-5.2 model, including its unique architecture features like MLA, DeepSeek Sparse Attention (DSA) with IndexShare, Mixture-of-Experts (MoE), and Multi-Token Prediction (MTP). The implementation correctly accounts for per-op precision, phase-specific asymptotics, and detailed kernel modeling. The accompanying design note and extensive test suite demonstrate a thorough understanding of the model and its performance characteristics.
### `gitm/optimizer/deviation.py`
**Improvement:**
* **Line 109:** The comment for `attn_kv_b` mentions "unabsorbed" MLA. While this is a crucial detail for the model, the comment itself is quite long and could be slightly condensed to focus on the mapping rationale. This is a minor stylistic suggestion.
```diff
--- a/gitm/optimizer/deviation.py
+++ b/gitm/optimizer/deviation.py
@@ -110,9 +110,9 @@ _OP_RULES: dict[str, tuple[str, ...]] = {
"attn_q_a": ("q_a_proj", "q_lora", "q_down"),
"attn_q_b": ("q_b_proj", "q_up"),
# `kv_b_proj` was absent here while the only MLA families modelled the
# *absorbed* decode form, where W^UK folds into the query and W^UV into the
# output projection and no such kernel is launched. The GLM-5.2 graph models
# it unabsorbed, so the kernel exists and has a node to land on. The entry is
# safe either way: an absorbed deployment launches nothing these needles
# match, so it stays absent rather than mis-attributing.
- "attn_kv_b": ("kv_b_proj", "kv_up", "w_uk", "w_uv"),
+ "attn_kv_b": ("kv_b_proj", "kv_up", "w_uk", "w_uv"), # For unabsorbed MLA
"qkv_proj": ("qkv",),
"attn_out_proj": ("o_proj", "out_proj", "attn_out"),
"mlp_gate_up": ("gate_up", "gate_proj", "up_proj", "swiglu", "silu_and_mul"),
```
### `gitm/planner/glm_graph.py`
No issues found. The new file is well-structured, thoroughly commented, and correctly implements the complex GLM-5.2 architecture. The logic for handling different phases (prefill, decode, MTP), precision overrides, and sharding is robust.
### `gitm/planner/model_catalogue.py`
No issues found. The `extends` feature is a valuable addition for managing model variants, and the validation for schedule lengths is correctly implemented.
### `gitm/planner/models/glm-5.2-fp8.yaml`
No issues found. The catalogue entry correctly leverages the `extends` feature and specifies the FP8-specific overrides and provenance.
### `gitm/planner/models/glm-5.2.yaml`
No issues found. The catalogue entry accurately reflects the BF16 GLM-5.2 model's architecture and schedules.
### `gitm/planner/registry.py`
No issues found. The family detection logic correctly prioritizes GLM-5.2, and the `_render_table` improvements provide much-needed detail for analysis.
### `tests/test_deviation_alignment.py`
No issues found. The test update correctly reflects the new `rms_norm` mapping.
### `tests/test_glm_graph.py`
No issues found. This is an exceptionally thorough and well-designed test suite that covers a wide range of architectural details, edge cases, and interactions. It provides strong confidence in the correctness of the GLM-5.2 implementation.
---
**Overall Summary:**
This is an excellent set of changes. The implementation is robust, well-tested, and clearly documented. The attention to detail in modeling the GLM-5.2 architecture, including its unique performance characteristics, is commendable. The new `extends` feature in the catalogue and the enhanced `gitm plan` output significantly improve the tool's capabilities and usability.
|
Best point in the second review: the note documents that tokens_per_step's 1 + D*alpha overstates a speculative step, then ships a CLI that prints the overstated number with no label. Anyone running `gitm plan --spec-tokens 5` and not reading the note gets 0.140 as break-even when it is 0.415. The plan table now says so when D > 0, and quantifies it when an acceptance rate is given (new --acceptance-rate, report-only — it cannot move the step floor). Still not changing BatchConfig: the convention is shared with every family. Doc consistency, also from the review: - §4.2's prefill attention row asserted "whole cache once per request" as fact while A9 called it an optimistic floor. The row and the §2.1 diagram now carry the caveat, so a tiled-kernel measurement reads as a known branch and not an anomaly. - Labelled the verify row "backbone only" — 1,591 excludes the draft, and the reviewer read it twice as though it might not. - The headline 1,933 tok/s carries an [A8] marker; ep_imbalance = 1.0 propagates through every MoE byte term and was silent on the summary. - §6 gets G10 as a stated precondition: the classification rule needs serial-vs-parallelisable, and nothing reads expected_stream_id today. - Chunk size: gave the derivable rule (bank costs 95.7 GB x ceil(P/C), so bytes scale as 1/C) instead of inventing a recommended value. Declined: pinning a commit hash at doc-generation time (the note is hand-written, the branch is the pin), an ep_imbalance uncertainty range (would mean inventing measurements), and a prompt-injection caveat on the vendor's tool-call flags (quoted to make the hardware assumption auditable, not as a production recipe). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 2a010fb. The best point was the one about the CLI: the note documents that Still not changing Also fixed: §4.2's prefill attention row asserted "whole cache once per request" as fact while A9 called it an optimistic floor — the row and the §2.1 diagram carry the caveat now, so a tiled-kernel measurement reads as a known branch rather than an anomaly. Labelled the verify row "backbone only" (1,591 excludes the draft — read as ambiguous twice, so that's on me). Put an Not doing three:
G11 has no owner or linked issue — flagging that for a human rather than opening one myself. 🤖 Addressed by Claude Code |
🤖 Claude Code ReviewThis is a design/documentation note (markdown only), not executable code. The diff contains no Python, CUDA, shell scripts, or other runnable artifacts—it's entirely prose, tables, and Mermaid diagrams describing a predicted execution model. 🐛 Bugs
📊 Reproducibility
💡 Suggestions
|
|
Condensed it — six lines of why the entry used to be absent, down to three lines of why it's safe now. Didn't take the diff as written: it kept the block and added a trailing 🤖 Addressed by Claude Code |
Six lines of why the entry used to be absent, where three lines of why it is safe now will do. Kept the load-bearing half: absorbed MLA launches no such kernel, so these needles match nothing there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code Review by GeminiHere's a review of the code changes, focusing on bugs and potential issues, with suggested improvements and exact line references.
|
🤖 Claude Code ReviewLooking at this diff, it's entirely a documentation file ( 🐛 Bugs
⚡ Performance
📊 Reproducibility
💡 Suggestions
|
Code Review by GeminiThe changes introduce support for the GLM-5.2 model, including its unique architecture features like DeepSeek Sparse Attention (DSA), IndexShare, and Multi-Token Prediction (MTP). The commit body and design note are exceptionally detailed, outlining the architectural nuances, performance predictions, and validation steps. The code implements these features, including per-op precision, specific prefill and MTP logic, and detailed node emission. I've identified one bug related to precision overrides when loading from a raw Bug:
|
… row Bounding the prefill-tile question reversed my own call from last round. I promoted it to Q3 on the reviewer's "64x error" framing without computing it. The prefill attention core is 0.10% of prefill bytes, so even a 128-row tiling takes the step from 422 GB to 448 GB — 1.1x, and it does not move the prefill conclusion. Demoted back to last, now with the number attached so nobody promotes it again on the multiplier alone. The good catch this round: the vendor recipe does not set --max-num-batched-tokens, so the 264.0 ms prefill floor silently assumed one 8,192-token chunk. Said so, added the 4 x 2,048 row (707 GB, 319 ms), and pointed at C6 to confirm what the engine actually uses. Also: struck the linear break-even row so it cannot be skimmed as usable (the CLI now warns too, and the row is kept only so the discrepancy is recognisable in that output); footnoted why alpha=0 is the same in both columns; gave S1 a forward reference to the §5.2 fork it resolves; fixed the ep_imbalance note to state the direction — over-predicted traffic means the throughput figures are conservative, not optimistic; and noted in G10 that expected_stream_id defaults to 0, which is indistinguishable from an explicit compute-stream assignment, so whoever wires the invariant should make it int | None first. Gemini's claimed _OP_RULES ordering bug is not one: embed_tokens is line 158, lm_head 176. It read the diff hunks rather than the file, and test_every_needle_is_reachable covers exactly this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 4d4b394. Gemini's Bounding Q3 reversed my own call from last round. I promoted it on the "64× error" framing without computing it. The prefill attention core is 0.10% of prefill bytes, so even a 128-row tiling takes the step from 422 GB to 448 GB — 1.1×, and it doesn't move the prefill conclusion. Demoted back to last, with the number attached so it doesn't get promoted again on the multiplier alone. My mistake for acting on the magnitude without checking the share. The chunking catch was the good one. The vendor recipe doesn't set Also: struck the linear break-even row so it can't be skimmed as usable — it's kept only so the discrepancy is recognisable in CLI output, which now warns as well. Footnoted why α=0 reads the same in both columns (it's the cost of drafting for nothing, and it's below the MTP-off baseline — that's the point of the column). Gave S1 a forward reference to the §5.2 fork it resolves. Corrected the Not doing: the pinned commit hash (third time — it's a hand-written note, the branch is the pin) and the Mermaid-portability suggestion (GitHub renders Mermaid natively). 🤖 Addressed by Claude Code |
🤖 Claude Code ReviewReviewThis is a design/analysis document (markdown), not executable code. The diff contains no Python, CUDA, CuPy, PyTorch, or bash code that runs — it's a working note describing a roofline model for GLM-5.2. Feedback is scoped accordingly. 🐛 Bugs
🔒 Security
⚡ Performance
📊 Reproducibility
💡 Suggestions
|
|
ERROR: # Code Review by Gemini |
embed_tokens mapped onto the lm_head op, so the untied halves were priced
together. Right for GLM-5.2, where both are in modules_to_not_convert, and
silently wrong for any checkpoint that quantised one and not the other —
dtype_for("embed_tokens") answered fp8 on a model that explicitly does not
convert it. The node itself was bf16 via act_dtype, so nothing was
mispriced today; the question just returned a wrong answer.
Now its own op: the footprint prices the two tensors separately, the
gather reads at the table's width and writes at the activation width, and
the fp8 catalogue entry declares it. Footprints unchanged (755.9 / 1508.1
GB) because both halves are bf16 here; a test pins that quantising the
embedding moves the footprint by the size of the table rather than by
nothing.
Also relabelled the §3.3 MTP rows — three reviews read 1,591 vs 1,614 as
ambiguous, so "vanilla decode (D=0), draft stage included" now says it on
the row instead of only in the note below, and the baseline is marked as
carrying no acceptance convention (tokens_per_step degenerates to batch at
D=0).
gemini-review is failing on a 503 from Google's API, not on this branch —
the action posted the error itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed e11ee7b.
The break-even baseline claim is wrong. 1,933 tok/s is measured at D=0, where Node counts: relabelled rather than re-explained. Three reviews have now read 1,591 vs 1,614 as ambiguous, which means the note below the table wasn't doing the job. The row now says "vanilla decode (D=0), draft stage included" on the row itself. The truncation is the diff view, not the file — fourth time. §9 ends "a residual is a lead, not a defect", Appendix A.1–A.5 follow it, and the file is 1,054 lines. Nothing parses this document to generate CLI help. On the CLI warning being unverified: it's tested —
🤖 Addressed by Claude Code |
Code Review by GeminiThe changes introduce support for the GLM-5.2 model, including its unique architecture features like DeepSeek Sparse Attention (DSA), IndexShare, per-op precision, and a MoE-based Multi-Token Prediction (MTP) draft. The accompanying The code is well-structured, highly detailed, and addresses several complex architectural nuances. The tests are comprehensive and cover critical aspects and potential pitfalls. Here are some minor suggestions and observations:
|
Second review to reconstruct the draft chain from the expert bank alone and find time missing. Their 0.804 ms is exactly the moe_routed term (0.803); the other 0.680 ms is lm_head, mtp_eh_proj and the attention block, itemised in A.4 the whole time. Twice now A.4 has not been reached from §3.3, so the split is in §3.3: 54% expert bank, 17% vocabulary projection, 11% eh_proj, 18% the rest. That also makes the section's own point better than the prose did — the draft is not just an expert-bank cost, and the two BF16 tensors it drags along are more than a quarter of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 2db5f21. The 1.483 ms is fully accounted — your 0.804 ms is exactly one of its terms.
But that's twice now someone has reconstructed the chain from the expert bank alone and found time missing, so A.4 clearly isn't being reached from §3.3. The split is now in §3.3 itself: 54 % expert bank, 17 % vocabulary projection, 11 % That states the section's own point better than the prose did, incidentally — the draft isn't purely an expert-bank cost, and the two BF16 tensors it drags along ( The 🤖 Addressed by Claude Code |
🤖 Claude Code ReviewReviewThis is a design/analysis document, not executable code. No Python, CUDA, CuPy, PyTorch, or bash logic is introduced—only prose, tables, Mermaid diagrams, and CLI invocations. The feedback below is scoped accordingly. 🐛 Bugs
🔒 Security
⚡ Performance
📊 Reproducibility
💡 Suggestions
|
Code Review by GeminiThe changes introduce comprehensive support for the GLM-5.2 model, including its unique architecture features like MLA, DeepSeek Sparse Attention (DSA), and Multi-Token Prediction (MTP). The implementation is highly detailed, addressing nuances such as per-op precision, distinct prefill/decode behaviors for DSA, and the MTP prefix chain acceptance rate. The accompanying design note is exceptionally thorough, and the new test suite is robust, covering a wide array of architectural specifics and potential edge cases. Here are the identified issues and suggested improvements: File:
|
Three clarity items, no behaviour change. The break-even cell gave alpha > 0.426 without saying what it crosses; it now says the tok/s row meets 1,969 there, which is the MTP-off rate a row below. Seventh reading of the verify row as though its node count should scale with rows. Verify is the backbone at 6x the rows and the same node count — more work per kernel, not more kernels — which is also why its bytes move and its nodes do not. Said on the row. Gemini asked how embed_tokens shards, and the note only ever stated it for lm_head. The code divides both by tp because vLLM builds the input table as VocabParallelEmbedding, split by vocabulary the same way the output projection is, and both sibling families already price it that way. Now stated in §3.1 rather than left to be inferred from the code — this is the third time it has been asked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 1e73a95. Three clarity fixes, no behaviour change. Break-even cell now says what it crosses: Verify node count. Verify is the backbone at 6× the rows and the same node count — more work per kernel, not more kernels. That's also why its bytes move (106.43 vs 67.34 GB) and its node count doesn't. Now on the row rather than inferable from it. Embedding sharding. Fair — the note stated it for 🤖 Addressed by Claude Code |
Code Review by GeminiThe code changes introduce support for the GLM-5.2 model, including its unique architecture features like DeepSeek Sparse Attention (DSA), IndexShare, Multi-Layer Attention (MLA), and a Mixture-of-Experts (MoE) draft head for Multi-Token Prediction (MTP). The changes are comprehensive, covering model specification, graph prediction, hardware context, catalogue entries, and extensive testing. The design note is exceptionally detailed and clearly outlines the architectural assumptions, predicted execution model, identified planner gaps, and validation plan. This level of documentation significantly aids in understanding the rationale behind the code changes. Overall, the implementation is robust and directly addresses the complexities of the GLM-5.2 model. The identified planner gaps (G1-G9) and bugs found during development are explicitly handled in the code, and the new test suite provides excellent coverage for these specific features and fixes. No critical bugs or issues were found that were not already acknowledged and addressed in the commit message or design note.
|
weight_bytes("fp8") is 1.000244 because of the 128x128 block scales stored
with fp8 weights. An fp8 KV cache carries a per-token or per-tensor scale
instead, so using the weight-side constant for cache bytes is the wrong
one -- 52,618 against a truer 52,608.
Left the number alone: 0.02% moves nothing, and changing kv_bytes_per_token
would mean a cache-specific byte constant that no other family has and
that no measurement here could distinguish. Named in the formula block so
it is a known approximation rather than an unexamined one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Pushed 3a1e191. One item had substance. The fp8 constant in the KV formula is borrowed from the wrong side. The MTP-off row is no longer the conflation you describe. That was true two revisions ago; since Not changing "B" to "G" for parameters. 744 B means 744 billion, which is the universal convention for LLM parameter counts (7B, 70B, 405B). "744 G parameters" isn't a form anyone uses. The 🤖 Addressed by Claude Code |
|
ERROR: # Code Review by Gemini |
Two problems, both in the review workflows rather than in any branch. Failing on upstream errors. claude-review exits 1 on any HTTPError, so an expired key or a 429 blocks the PR; gemini-review's action exits non-zero on Gemini's 503, which happened five times on #104. An advisory reviewer that cannot reach its API has found nothing -- it has not found a problem. Both now retry the transient classes (429, 5xx, network, timeout) with backoff and then pass with a ::warning:: rather than a red check. 4xx that will fail identically on retry -- auth, billing, malformed -- are not retried, so a dead key costs one call, not three. lint and pytest stay blocking; they are the checks that say something about the code. The bot was never shown the code. The diff was filtered to '*.py' '*.sh' '*.md' '*.yaml' '*.yml' in one git diff, which orders by path, then truncated to 75KB. On #104 that is 88KB of docs/ ahead of 132KB of gitm/, so every one of ~19 reviews saw .github and part of DESIGN-NOTE.md and never a line of Python -- which is why they all opened "this is a documentation-only diff" and why every finding was about prose. Code paths are now diffed first and prose appended, so truncation drops prose. Also told the model the diff may be truncated, since several reviews inferred things from what was absent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
ERROR: # Code Review by Gemini |
# Conflicts: # gitm/planner/context.py # gitm/planner/model_catalogue.py # gitm/planner/registry.py
Code Review by GeminiThis pull request introduces comprehensive support for the GLM-5.2 model, including its unique architectural features like DeepSeek Sparse Attention (DSA), IndexShare, and Multi-Token Prediction (MTP). The changes are well-justified by the detailed 🐛 Bugs
⚡ Performance
💡 Suggestions
|
Code Review by GeminiThe changes introduce support for the GLM-5.2 model, including its unique architecture (MLA + DeepSeek Sparse Attention, IndexShare, MoE, MTP), mixed-precision handling, and specific prefill/decode characteristics. The pull request is exceptionally well-documented, with a detailed design note and comprehensive test coverage that directly validates the architectural claims and bug fixes. Here's a breakdown of the review: 🐛 Bugs
⚡ Performance
💡 Suggestions
Overall AssessmentThis is an exceptionally well-executed and documented pull request. The commit message and the The identified bugs are correctly addressed, and the performance improvements are well-justified. The suggestions are minor and mostly related to documentation clarity or known future work. This PR sets a high standard for model integration and planning. |
Code Review by GeminiThe changes in this pull request introduce support for the GLM-5.2 model family, including its unique architecture (MLA + DSA, IndexShare, MoE with specific precision overrides), prefill behavior, and multi-token prediction (MTP) chain. The commit message is exceptionally detailed, outlining the design rationale, bugs found and fixed, and open questions. The code reflects a thorough implementation of these architectural details and addresses several subtle issues identified during development. Here's a breakdown of the review: 🐛 BugsThe commit body explicitly lists seven bugs found and fixed during development. The code changes directly address all of them:
🔒 SecurityNo security-related issues were identified in the changes. The modifications primarily involve model graph prediction logic and CI workflow robustness. ⚡ PerformanceThe core of this pull request is about accurate performance modeling. Several changes directly improve the fidelity of performance predictions:
📊 Reproducibility
💡 SuggestionsThe code is exceptionally well-documented, and the commit message anticipates most potential questions and concerns. The new test suite ( One minor observation:
Overall, this is a high-quality and impactful pull request that significantly extends the capabilities of the |
|
ERROR: # Code Review by Gemini |
Code Review by GeminiThe changes introduce support for the GLM-5.2 model, including its unique architecture (MLA+DSA, IndexShare, MoE with specific precision overrides), prefill logic, and multi-token prediction (MTP) chain. The pull request also includes significant improvements to the Here's a review of the changes: 🐛 BugsThe commit body explicitly lists several bugs found and fixed during development, and the code changes reflect these fixes.
⚡ Performance
💡 Suggestions
|
|
ERROR: # Code Review by Gemini |
Code Review by Gemini🤖 Code ReviewThis is a substantial and well-documented change, addressing a new model architecture ( 🐛 Bugs
⚡ Performance
💡 Suggestions
Overall ImpressionThis is an exceptionally thorough and well-engineered set of changes. The level of detail in the design note and the corresponding code implementation is commendable. The new GLM-5.2 model is complex, and the changes correctly capture its unique architectural features and their performance implications. The self-correction and validation against multiple checkpoints and scenarios (prefill, decode, MTP, different precisions) demonstrate a deep understanding of the problem space. The test suite for The identified suggestions are minor and mostly about code clarity or very subtle edge cases, not critical bugs. --- a/gitm/planner/glm_graph.py
+++ b/gitm/planner/glm_graph.py
@@ -300,7 +300,8 @@
# Reconstruct per-head K_nope and V from the cached latent (W^UK, W^UV),
# modelled *unabsorbed*: its own GEMM, and attn_out_proj stays narrow. An
# engine that absorbs MLA drops this node and doubles attn_out_proj's input
-# width instead — a serving variant, flagged in the catalogue provenance.
+# width instead (e.g., `attn_out_proj` input becomes `n_heads * (v_head_dim + kv_lora_rank)`).
+# This is a serving variant, flagged in the catalogue provenance.
f, b = _linear(
rows, spec.kv_lora_rank,
spec.n_heads * (spec.qk_nope_head_dim + spec.v_head_dim) // tp, aw,
@@ -579,7 +580,7 @@
# skipped only the norm and quantised ``indexers_proj`` would be read as
# leaving the whole indexer bf16.
("indexers_proj", "attn_index_proj"),
("indexer.wq_b", "attn_index_proj"),
("indexer.wk", "attn_index_proj"),
("indexer.weights_proj", "attn_index_proj"),
-("mlp.gate", "moe_router"),
+# ("mlp.gate", "moe_router"), # Redundant as moe_router_dtype is authoritative for moe_router's precision.
)
|
|
|
Models GLM-5.2 (
glm_moe_dsa) end to end and writes up the result. Everything is read fromconfig.jsonand the checkpoint's tensor index; no traces.Two catalogue entries:
glm-5.2(bf16, the model fact) andglm-5.2-fp8(753 GB, what the vendor actually recommends and the only one that fits 8xH200). The FP8 entryextendsthe bf16 one, so the two differ only in dtypes.Graph work
op_dtype_overrides), read frommodules_to_not_convertandmoe_router_dtype. The layout is backwards from the fp8 models we've seen:o_projis quantised, the indexer isn't.index_topkbound the core's FLOPs in both phases and its bytes in neither, so there are separate helpers for pairs vs entries. Aliasingattention_qk_pairshere is wrong in both directions at once and the errors partly cancel, which is what makes it dangerous.lm_headper stage (--spec-tokens,--acceptance-rate). GLM's draft block carries a full 256-expert bank, so a stage is 54% expert weights and the chain is 5.3% of the step, not the 1–2% a dense draft head costs.Bugs found while building it
Seven, all of the same shape — two numbers that should agree, derived independently, with nothing checking that they did. Each fix is an identity test rather than a pinned constant.
indexer_typesin the catalogue had 77 entries for 78 layers. Layer 77 took the modulo fallback, landed on the right answer, and the floor was byte-identical — so nothing failed. The loader validates schedule length now.attn_index_scorehad lost its 32-head factor in the prefill rewrite. Doesn't move decode (memory-bound either way); doubles the prefill row.model_weight_bytescounted an indexer for the MTP block, which carries none. The graph emitted 21 and the footprint counted 22 — 18.7 MB on a 1.5 TB model, invisible to any size check.embed_tokensmapped onto thelm_headop, sodtype_for("embed_tokens")answered fp8 on a checkpoint that explicitly doesn't convert it. Right number, wrong question.indexerneedle also matchedindexer.k_norm, so a norm in the skip list marked the whole indexer bf16. Every fp8 scheme leaves norms wide, so that name carries no information.--speculative-confignever does.BatchConfig.tokens_per_stepcounted1 + D·α, the independent-draws answer, where a verifier accepts a prefix. Overstated throughput 1.8× at D=5, α=0.5 and put break-even at less than a third of its real value. Shared with every family and wrong for all of them.A conclusion that changed
Prefill isn't memory-bound. The whole-pass AI divided FLOPs by HBM bytes and NVLink payload together, then compared that to a ridge derived from HBM bandwidth. Against HBM alone it's AI 410 against a ridge of 412 — balanced — and 56% of the floor is wire. The label is comm, and it reverts to HBM-bound under TP8-only, which is capture C5.
Outside the family
gitm planreports accepted tokens rather thanbatch/step(so--acceptance-ratedoes something), and warns when a speculative step is priced without one._OP_RULESgains entries for the ops the pointwise lowering added, plusattn_index_proj(was shadowed by the scan's own needle),attn_kv_bandmtp_eh_proj. Left the gating and silu mappings alone — those are shared with the other families.docs/ate the whole 75 KB budget and ~19 reviews never saw a line of Python. Code is diffed first now, and API failures warn instead of blocking.Validation
Predicted weight bytes land within 0.1% of the bf16 checkpoint and 0.4% of the fp8 one. The third check is the useful one: Z.ai publishes 744B params, the checkpoint is 753.3B by its own bytes, and dropping the MTP block predicts 744.2B. That puts the block at 9.9B against 0.23B for a dense head — independent confirmation of the MoE-draft reading, from a number published for an unrelated reason.
Not done, deliberately
depends_onedges (G10). Cross-family IR change, already on the roadmap.expected_stream_idis set on collectives but nothing reads it today, so it's a hook, not a wiring — and it defaults to0, indistinguishable from an explicit compute stream, which whoever wires it should fix first.library.yaml's op vocabulary has nomoe_routed, so the levers meant for expert traffic scope to[mlp_gate_up, mlp_down]. That's 74% of a decode step unreachable by the tooling. Pre-existing and identical for DeepSeek-V4, so it should land where both families can be checked — written up as G11.Merge note
main's #101 (MiMo-V2.5) independently fixed the same fp32-peak, per-dtype-ridge and node-owned-bound gaps — both branches were working from the same design note. Took main's implementations for all three;BatchConfig.attention_qk_pairsalso became a method taking awindow, which auto-merged cleanly and then failed at runtime. GLM has no window, so it's called unwindowed.Deleted the three committed node-dump JSONs (29k lines that went stale every time the graph changed); the commands at the top of the note regenerate them.
Suite is at the pre-existing 4 failures (
test_bench,test_hft_intervention, two intest_importers) — verified on a clean tree.🤖 Generated with Claude Code